package com.xyy.saas.payment.adapter.channel.fm.client;

import com.alibaba.fastjson.JSONObject;
import com.xyy.saas.payment.adapter.channel.fm.config.Configuration;
import com.xyy.saas.payment.adapter.channel.fm.config.ModuleCode;
import com.xyy.saas.payment.adapter.channel.fm.constants.SDKVersion;
import com.xyy.saas.payment.util.AESUtils;
import com.xyy.saas.payment.util.encrypt.Encryptor;
import com.xyy.saas.payment.adapter.channel.fm.config.OpenParameters;
import com.xyy.saas.payment.util.HttpClientUtil;
import com.xyy.saas.payment.util.SerialNoUtil;
import com.xyy.saas.payment.util.VerifyAndDecryptUtil;
import lombok.extern.slf4j.Slf4j;

import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;

@Slf4j
public class FBankOpenApiClient {
    /**
     * 服务所使用的加密算法
     */
    private static final String ALGORITHM = "algorithm";
    /**
     * 加密算法服务类型
     **/
    private static final String ALGORITHM_TYPE = "rsaDetached";

    /**
     * 签名数据
     **/
    private static final String SIGN_DATA = "signData";

    /**
     * 配置
     */
    private Configuration config;

    public FBankOpenApiClient(Configuration config) {
        this.config = config;
    }

    private static Map<String, String> setHttpHeaders(OpenParameters context, String clientSerialNo) {
        Map<String, String> header = new HashMap<>(16);
        header.put("api_name", context.getApiName());
        String apiCode = context.getApiCode();
        if (null != apiCode) {
            header.put("api_code", apiCode);
        }
        String moduleCode = context.getModuleCode();
        if (null != moduleCode) {
            header.put("module_code", moduleCode);
        }
        header.put("api_version", context.getApiVersion());
        header.put(ALGORITHM, ALGORITHM_TYPE);
        header.put("client_serial_no", clientSerialNo);
        header.put("Content-Type", "application/json");
        return header;
    }

    private static String getResponse(String url, String encryptedBody, Map<String, String> header) throws Exception {
        return HttpClientUtil.xyyHttpPostRequest(url, encryptedBody, "application/json", header);
    }

    /**
     * 将某些特定的module_code转化为真正的请求
     *
     * @param context 请求参数上下文
     */
    private static String pseudoModuleCode(OpenParameters context, String address) {
        if (!address.endsWith("/")) {
            address += "/";
        }
        if (ModuleCode.OPENAPI_FUNCTION.rawValue().equalsIgnoreCase(context.getModuleCode())) {
            address += "?function=" + context.getApiCode();
        }
        return address;
    }

    public Configuration getConfig() {
        return config;
    }

    public void setConfig(Configuration config) {
        this.config = config;
    }

    /**
     * 建造者模式发送请求
     * We recommend to use this method.
     *
     * @param context params from OpenContext.builder
     * @return response from server
     * @throws Exception all exceptions
     * @since 2.0
     */
    public String send(OpenParameters context) throws Exception {
        return sendBuild(context, context.getUserId(), context.getParam());
    }

    private String sendBuild(OpenParameters context, String userId, String params) throws Exception {
        // 对请求参数进行加密和签名
        String encryptedBody = buildBody(userId, params);
        // 校验请求流水号
        String clientSerialNo = context.getSerialNo();
        if (null == clientSerialNo || clientSerialNo.isEmpty()) {
            clientSerialNo = SerialNoUtil.calculateSerialNo(params);
        }
        String encryptedResult = fetch(context, encryptedBody, clientSerialNo);
        JSONObject jsonObject = JSONObject.parseObject(encryptedResult);
        if (null == jsonObject || !jsonObject.containsKey(SIGN_DATA)) {
            return encryptedResult;
        }
        // 使用双证书版本的公钥进行加密
        String fbankSignKey = config.fbankSignPubKey();
        // 签名验证，失败抛出异常
        JSONObject json = VerifyAndDecryptUtil.verifySignature(encryptedResult, fbankSignKey);
        // 解密返回结果
        String result = VerifyAndDecryptUtil.decryptResult(json, config.privateKey());
        // LOGGER.info("FBankOpenApiClient#sendBuild, encryptedBody:{}, result:{}", encryptedBody, result);
        return result;
    }

    /**
     * 获取响应结果
     */
    private String fetch(OpenParameters context, String encryptedBody, String clientSerialNo) throws Exception {
        String path = pseudoModuleCode(context, config.remoteAddress());
        Map<String, String> header = setHttpHeaders(context, clientSerialNo);
        // 读取响应结果，关闭输入输出流
        return getResponse(path, encryptedBody, header);
    }

    /**
     * 对请求参数进行加密和签名
     *
     * @param userId 用户ID
     * @param json   http request parameters
     * @return 加密和签名后的json字符串
     */
    private String buildBody(String userId, String json) throws Exception {
        // 生成AES 随机密钥
        String randomKey = AESUtils.randomKey();
        Encryptor encryptor = VerifyAndDecryptUtil.getEncryptor(config.encryptType());
        // 对AES密钥加密
        String encryptedRandomKey;
        // 使用双证书版本的公钥进行加密
        encryptedRandomKey = encryptor.encrypt(randomKey, config.fbankEncPubKey());

        if (null == json || json.isEmpty()) {
            // A stupid hack for server mustn't be nil.
            JSONObject jsonObject = new JSONObject();
            json = JSONObject.toJSONString(jsonObject);
        }
        // 用AES密钥对请求参数加密
        String data = AESUtils.encrypt(json, randomKey);
        TreeMap<String, Object> tempMap = new TreeMap<>();
        tempMap.put("data", data);
        tempMap.put("randomKeyEncrypt", encryptedRandomKey);
        tempMap.put("timestamp", System.currentTimeMillis());
        tempMap.put("merchantNo", config.merchantNo());
        tempMap.put("channelNo", config.channelNo());
        tempMap.put("signType", config.signType());
        tempMap.put("encryptType", config.encryptType());
        tempMap.put("sdkVersion", SDKVersion.SDK_VERSION);
        if (null != config.appId() && !config.appId().isEmpty()) {
            tempMap.put("appId", config.appId());
            // if appId, we must also have siteId.
            tempMap.put("siteId", config.siteId());
        }
        if (null != userId && !userId.isEmpty()) {
            tempMap.put("userId", userId);
        }
        JSONObject jsonObject = new JSONObject(tempMap);
        String signStr = jsonObject.toJSONString();
        Encryptor signCryptor = VerifyAndDecryptUtil.getEncryptor(config.signType());
        // 对加密后的输入参数签名
        String signedStr = signCryptor.signature(signStr);
        jsonObject.put(SIGN_DATA, signedStr);
        return jsonObject.toJSONString();
    }
}